# application: File Monitor
# author: Geo Petrini
# version: 0.5.3
# last update: 2008.12.02

import os.path
import sys
import time

from configobj import ConfigObj
import hashlib
import logging
import logging.config
import logging.handlers
import os
import traceback

configDir		= r".\config"

configFile		= os.path.join(configDir, 'filemonitor.ini')
logger			= None

def main(argv):
	global logger
	logger = initLogger()
	
	printandlog("script started", logging.INFO)

	jobids = getJobIds()
	printandlog("jobs: " + str(jobids), logging.DEBUG)

	for jobid in jobids:
		runJob(jobid)

	printandlog("end of script", logging.INFO)

def runJob(jobid):
	if jobid != None and jobid != "" and os.path.isfile(configFile):
		printandlog("processing: " + jobid, logging.INFO)

		fileids = getJobFileIds(jobid)
		actionids = getJobActionIds(jobid)

		printandlog("[" + jobid + "]" + "fileids: " + str(fileids), logging.DEBUG)
		printandlog("[" + jobid + "]" + "actionids: " + str(actionids), logging.DEBUG)

		conf = ConfigObj(configFile)

		for fileid in fileids:
			filedata = getFileData(jobid, fileid)

			printandlog("[" + jobid + "]" + "[" + fileid + "]" + "filedata: " + str(filedata), logging.DEBUG)

			scan_time = time.time()
			conf[jobid][fileid]["last_scan"] = scan_time
			#cur_mtime_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(filedata['cur_mtime']))
			conf[jobid][fileid]["last_scan_str"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(scan_time))
			conf.write()

			for actionid in actionids:
				triggers = None
				commands = None
				
				triggers = getActionTriggers(jobid, actionid)
				if triggers != None:
					printandlog("[" + jobid + "]" + "[" + actionid + "]" + "triggers: " + str(triggers), logging.DEBUG)
				else:
					printandlog("[" + jobid + "]" + "[" + actionid + "]" + "triggers: None", logging.WARNING)

				commands = getActionCommands(jobid, actionid)
				if commands != None:
					printandlog("[" + jobid + "]" + "[" + actionid + "]" + "commands: " + str(commands), logging.DEBUG)
				else:
					printandlog("[" + jobid + "]" + "[" + actionid + "]" + "commands: None", logging.WARNING)


				if triggers != None and commands != None:
					if fireAction(triggers, filedata):
						printandlog("[" + jobid + "]" + "[" + actionid + "]" + ": fired", logging.INFO)
						# run commands for this job
						runCommands(commands)
					else:
						printandlog("[" + jobid + "]" + "[" + actionid + "]" + ": not fired", logging.INFO)
			
			#update data
			if filedata['file_exists']:
				if 'cur_size' in filedata:
					conf[jobid][fileid]['rec_size'] = filedata['cur_size']
				if 'cur_ctime' in filedata:
					conf[jobid][fileid]['rec_ctime'] = filedata['cur_ctime']
					conf[jobid][fileid]['rec_ctime_str'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(filedata['cur_ctime']))
				if 'cur_mtime' in filedata:
					conf[jobid][fileid]['rec_mtime'] = filedata['cur_mtime']
					conf[jobid][fileid]['rec_mtime_str'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(filedata['cur_mtime']))
				if 'cur_md5' in filedata:
					conf[jobid][fileid]['rec_md5'] = filedata['cur_md5']

				conf.write()

	else:
		printandlog("invalid job", logging.INFO)

def getFileData(jobid, fileid):
	conf = ConfigObj(configFile)

	filedata = {}

	filename = conf[jobid][fileid]["filename"]
	filedata['filename'] = filename

	rec_size = conf[jobid][fileid]["rec_size"]
	if rec_size == "":
		rec_size = 0
	rec_size = long(rec_size)
	filedata['rec_size'] = rec_size

	rec_ctime = conf[jobid][fileid]["rec_ctime"]
	if rec_ctime == "":
		rec_ctime = 0
	rec_ctime = float(rec_ctime)
	filedata['rec_ctime'] = rec_ctime

	rec_mtime = conf[jobid][fileid]["rec_mtime"]
	if rec_mtime == "":
		rec_mtime = 0
	rec_mtime = float(rec_mtime)
	filedata['rec_mtime'] = rec_mtime

	rec_md5 = conf[jobid][fileid]["rec_md5"]
	filedata['rec_md5'] = rec_md5

	last_scan = conf[jobid][fileid]["last_scan"]	#actually there is no need to load this

	file_exists = os.path.isfile(filedata['filename'])
	filedata['file_exists'] = file_exists

	if file_exists:
		statinfo = os.stat(filedata['filename'])
		filedata['cur_size'] = statinfo.st_size
		filedata['cur_ctime'] = statinfo.st_ctime
		filedata['cur_mtime'] = statinfo.st_mtime
		try:
			fp = open(filename, "rb")
			h = hashlib.md5(fp.read())
			md5digest = h.hexdigest() #hashlib.md5(fp.read()).hexdigest
			filedata['cur_md5'] = md5digest
			fp.close()
		except:
			printandlog("error computing MD5 on :" + filename, logging.ERROR)
			traceback.print_exc(file=sys.stdout)

	return filedata

def fireAction(triggers, filedata):
	result = False
	for trigger in triggers:
		#TODO parse trigger
		#TODO check trigger against file data
		# possible triggers:
		#   deleted
		#   created
		#   size_changed[optional > / < / = / != #number of bytes]
		#   ctime_changed[optional > / < / = / != #number of seconds]
		#   mtime_changed[optional > / < / = / != #number of seconds]
		#	size_is(not implemented yet)
		#   md5_changed(not implemented yet)

		if trigger == "":
			return False
		
		if trigger == "deleted":

			printandlog("checking 'deleted' trigger", logging.DEBUG)

			if checkIsDeleted(filedata):
				result = True
				printandlog("'deleted' trigged", logging.DEBUG)
			else:
				printandlog("'deleted' not trigged, ending check", logging.DEBUG)
				return False

		if trigger == "created":

			printandlog("checking 'created' trigger", logging.DEBUG)

			if checkIsCreated(filedata):
				result = True
				printandlog("'created' trigged", logging.DEBUG)
			else:
				printandlog("'created' not trigged, ending check", logging.DEBUG)
				return False

		if trigger == "md5_changed":

			printandlog("checking 'md5_changed' trigger", logging.DEBUG)

			if checkMD5Changed(filedata):
				result = True
				printandlog("'md5_changed' trigged", logging.DEBUG)
			else:
				printandlog("'md5_changed' not trigged, ending check", logging.DEBUG)
				return False

		if trigger.startswith("size_changed"):

			printandlog("checking 'size_changed' trigger", logging.DEBUG)

			if checkSizeChanged(filedata, trigger):
				result = True
				printandlog("'size_changed' trigged", logging.DEBUG)
			else:
				printandlog("'size_changed' not trigged, ending check", logging.DEBUG)
				return False

		if trigger.startswith("ctime_changed"):

			printandlog("checking 'ctime_changed' trigger", logging.DEBUG)

			if checkCTimeChanged(filedata, trigger):
				result = True
				printandlog("'ctime_changed' trigged", logging.DEBUG)
			else:
				printandlog("'ctime_changed' not trigged, ending check", logging.DEBUG)
				return False

		if trigger.startswith("mtime_changed"):

			printandlog("checking 'mtime_changed' trigger", logging.DEBUG)

			if checkMTimeChanged(filedata, trigger):
				result = True
				printandlog("'mtime_changed' trigged", logging.DEBUG)
			else:
				printandlog("'mtime_changed' not trigged, ending check", logging.DEBUG)
				return False

		if trigger.startswith("size_is"):

			printandlog("checking 'size_is' trigger", logging.DEBUG)
			
			if checkSizeIs(filedata, trigger):
				result = True
				printandlog("'size_is' trigged", logging.DEBUG)
			else:
				printandlog("'size_is' not trigged, ending check", logging.DEBUG)
				return False

	if result == True:
		return result
	else:
		return False

def parseArg(arg):
	args = {}
	arg = arg.replace("(", "")
	arg = arg.replace(")", "")
	arg = arg.replace(" ", "")
	if arg.startswith("<"):
		args['compare'] = "<"
		args['value'] = arg[1:]
	if arg.startswith(">"):
		args['compare'] = ">"
		args['value'] = arg[1:]
	if arg.startswith("="):
		args['compare'] = "="
		args['value'] = arg[1:]
	if arg.startswith("!="):
		args['compare'] = "!="
		args['value'] = arg[2:]
	
	return args

def checkIsDeleted(filedata):
	if filedata['file_exists'] == False:
		return True
	else:
		return False

def checkIsCreated(filedata):
	if filedata['file_exists'] == True:
		return True
	else:
		return False

def checkMD5Changed(filedata):
	if filedata['file_exists'] == True:

		if 'cur_md5' in filedata and 'rec_md5' in filedata:
			if filedata['cur_md5'] != filedata['rec_md5']:		#md5 changed?
				return True
			else:
				return False

		elif 'cur_md5' in filedata and not 'rec_md5' in filedata:
			#since we only have the cur_md5, md5 has changed
			return True
		
		elif not 'cur_md5' in filedata and 'rec_md5' in filedata:
			#TODO make sure to document this because it can be strange
			#since we don't have the cur_md5, something has happened to the file, but we don't know anything about the md5
			return False	
	else:
		return False


def checkSizeChanged(filedata, trigger):
	if filedata['file_exists'] == True:
		delta = filedata['cur_size'] - filedata['rec_size']
		
		printandlog("size delta: " + str(delta), logging.DEBUG)


		arg = trigger[len("size_changed"):]
		args = parseArg(arg)
		printandlog("'size_changed' trigger args: " + str(args), logging.DEBUG)

		if 'compare' in args and 'value' in args:
			compare = args['compare']
			#TODO catch exception
			value = int(args['value'])
			if compare == "<":
				if delta < value:
					return True
			elif compare == ">":
				if delta > value:
					return True
			elif compare == "=":
				if delta == value:
					return True
			elif compare == "!=":
				if delta != value:
					return True
			else:
				printandlog("'size_changed' invalid compare argument: " + compare, logging.ERROR)
				return False
		elif delta != 0:
			return True
	else:
		return False


def checkCTimeChanged(filedata, trigger):
	if filedata['file_exists'] == True:
		delta = int(round(filedata['cur_ctime'] - filedata['rec_ctime']))	#round to 0 decimals and convert to int

		printandlog("ctime delta: " + str(delta), logging.DEBUG)

		arg = trigger[len("ctime_changed"):]
		args = parseArg(arg)
		printandlog("'ctime_changed' trigger args: " + str(args), logging.DEBUG)

		if 'compare' in args and 'value' in args:
			compare = args['compare']
			#TODO catch exception
			value = int(args['value'])
			if compare == "<":
				if delta < value:
					return True
			elif compare == ">":
				if delta > value:
					return True
			elif compare == "=":
				if delta == value:
					return True
			elif compare == "!=":
				if delta != value:
					return True
			else:
				printandlog("'ctime_changed' invalid compare argument: " + compare, logging.ERROR)
				return False
		elif delta != 0:
			return True
	else:
		return False

def checkMTimeChanged(filedata, trigger):
	if filedata['file_exists'] == True:
		delta = int(round(filedata['cur_mtime'] - filedata['rec_mtime']))	#round to 0 decimals and convert to int

		printandlog("mtime delta: " + str(delta), logging.DEBUG)

		arg = trigger[len("mtime_changed"):]
		args = parseArg(arg)
		printandlog("'mtime_changed' trigger args: " + str(args), logging.DEBUG)

		if 'compare' in args and 'value' in args:
			compare = args['compare']
			#TODO catch exception
			value = int(args['value'])
			if compare == "<":
				if delta < value:
					return True
			elif compare == ">":
				if delta > value:
					return True
			elif compare == "=":
				if delta == value:
					return True
			elif compare == "!=":
				if delta != value:
					return True
			else:
				printandlog("'mtime_changed' invalid compare argument: " + compare, logging.ERROR)
				return False
		elif delta != 0:
			return True
	else:
		return False

def checkSizeIs(filedata, trigger):
	# TODO test 
	if 'cur_size' in filedata:
		
		arg = trigger[len("size_is"):]
		args = parseArg(arg)
		printandlog("'size_is' trigger args: " + str(args), logging.DEBUG)

		if 'compare' in args and 'value' in args:
			compare = args['compare']
			#TODO catch exception
			value = int(args['value'])
			if compare == "<":
				if filedata['cur_size'] < value:
					return True
			elif compare == ">":
				if filedata['cur_size'] > value:
					return True
			elif compare == "=":
				if filedata['cur_size'] == value:
					return True
			elif compare == "!=":
				if filedata['cur_size'] != value:
					return True
			else:
				printandlog("'size_is' invalid compare argument: " + compare, logging.ERROR)
				return False
		else:
			printandlog("'size_is' trigger needs some arguments", logging.ERROR)
			return False
	else:
		return False

def runCommands(commands):
	for command in commands:
		printandlog("executing: " + command, logging.INFO)
		#TODO change to os.subprocess and test
		os.popen(command)


def getJobIds():
	jobids = []
	if os.path.isfile(configFile):
		conf = ConfigObj(configFile)
		keys = conf.keys()

		if  keys != None and len(keys) > 0:
			for key in keys:
				if key.startswith("job_"):
					jobids.append(key)

			return jobids
	else:
		printandlog("error configuration not found: " + configFile, logging.ERROR)

	return jobids

def getJobFileIds(jobid):
	fileids = []
	if os.path.isfile(configFile):
		conf = ConfigObj(configFile)
		keys = conf[jobid].keys()

		if  keys != None and len(keys) > 0:
			for key in keys:
				if key.startswith("file_"):
					fileids.append(key)

			return fileids
	else:
		printandlog("error configuration not found: " + configFile, logging.ERROR)

	return fileids

def getJobActionIds(jobid):
	actionids = []
	if os.path.isfile(configFile):
		conf = ConfigObj(configFile)
		keys = conf[jobid].keys()

		if  keys != None and len(keys) > 0:
			for key in keys:
				if key.startswith("action_"):
					actionids.append(key)

			return actionids
	else:
		printandlog("error configuration not found: " + configFile, logging.ERROR)

	return actionids

def getActionTriggers(jobid, actionid):
	if os.path.isfile(configFile):
		conf = ConfigObj(configFile)

		if "triggers" in conf[jobid][actionid]:
			triggers = conf[jobid][actionid]["triggers"]

			#convert to a list
			if not isinstance(triggers, list):
				templist = list()
				templist.append(triggers)
				triggers = templist
			return triggers
		else:
			return None
	else:
		return None


def getActionCommands(jobid, actionid):
	if os.path.isfile(configFile):
		conf = ConfigObj(configFile)

		if "commands" in conf[jobid][actionid]:
			commands = conf[jobid][actionid]["commands"]

			#convert to a list
			if not isinstance(commands, list):
				templist = list()
				templist.append(commands)
				commands = templist
			return commands
		else:
			return None
	else:
		return None

def initLogger():
	#setup file logger
	logging.config.fileConfig(os.path.join(configDir, "logger.conf"))

	l = logging.getLogger('filemonitor')

	return l
	
def printandlog(text, mode=logging.INFO):
	#print(text)	#not needed anymore since logger outputs to console as well as to file
	if logger == None:
		print text
	else:
		logger.log(mode, text)

if __name__ == "__main__":
	main(sys.argv)
